package com.yahoo.astra.fl.charts
{
	import com.yahoo.astra.fl.charts.series.ISeries;
	import fl.core.InvalidationType;
	import fl.core.UIComponent;
	
	import flash.display.Sprite;
	import flash.text.TextField;
	import flash.text.TextFieldAutoSize;
	import flash.text.TextFormat;
	
	/**
	 * An axis type representing a range of categories.
	 * 
	 * @author Josh Tynjala
	 */
	public class CategoryAxis extends Axis
	{
		
	//--------------------------------------
	//  Constructor
	//--------------------------------------
		
		/**
		 * Constructor.
		 */
		public function CategoryAxis()
		{
			super(this);
		}
	
	//--------------------------------------
	//  Variables and Properties
	//--------------------------------------
		
		/**
		 * @private
		 * The drawing canvas for the major grid lines.
		 */
		public var gridLines:Sprite;
	
		/**
		 * @private
		 * Used to determine positioning based on category.
		 */
		private var _categorySize:Number = 0;
	
	//-- Labels
		
		/**
		 * @private
		 * The maximum size of the labels. Comes from the width property when
		 * the axis is vertical, or the height if it is horizontal.
		 */
		private var _maxLabelSize:Number = 0;
		
		/**
		 * @private
		 * Storage for the labels.
		 */
		protected var labels:Array = [];
		
		/**
		 * @private
		 */
		private var _categoryLabels:Array = [];
		
		/**
		 * @private
		 * Storage for the categoryNames property.
		 */
		private var _categoryNames:Array = [];
		
		/**
		 * @private
		 * Indicates whether the category labels are user-defined or generated by the axis.
		 */
		private var _categoryNamesSetByUser:Boolean = false;
		
		/**
		 * The category labels to display along the axis.
		 */
		public function get categoryNames():Array
		{
			return this._categoryNames;
		}
		
		/**
		 * @private
		 */
		public function set categoryNames(value:Array):void
		{
			this._categoryNamesSetByUser = value != null;
			if(!value)
			{
				this._categoryNames = null;
			}
			else
			{
				//ensure that all category names are strings
				var names:Array = [];
				for(var i:int = 0; i < value.length; i++)
				{
					names.push(value[i].toString());
				}
				this._categoryNames = names;
			}
			this.invalidate();
		}
	
	//--------------------------------------
	//  Public Methods
	//--------------------------------------
	
		/**
		 * @copy com.yahoo.astra.fl.charts.IAxis#valueToLocal()
		 */
		override public function valueToLocal(data:Object):Number
		{
			if(data === null) return NaN;
			var index:int = this._categoryNames.indexOf(data.toString());
			var position:Number = 0;
			if(index >= 0)
			{
				if(this.orientation == AxisOrientation.VERTICAL)
				{
					position = this._categorySize * index + (this._categorySize / 2);
					if(!this.reverse) position = this._contentBounds.height - position;
				}
				else
				{
					position = this._categorySize * index + (this._categorySize / 2);
					if(this.reverse) position = this._contentBounds.width - position;
				}
			}
			else return NaN;
			
			//we don't understand this data!
			return position;
		}
		
		/**
		 * @copy com.yahoo.astra.fl.charts.IAxis#localToValue()
		 */
		override public function localToValue(position:Number):Object
		{
			throw new Error("CategoryAxis.localToValue is not implemented.");
			return 0;
		}
		
		/**
		 * @copy com.yahoo.astra.fl.charts.IAxis#valueToLabel();
		 */
		override public function valueToLabel(category:Object):String
		{
			var categoryText:String = category.toString();
			if(this.labelFunction != null)
			{
				categoryText = this.labelFunction(category);
			}
			
			if(categoryText === null) categoryText = "";
			return categoryText;
		}
	
		/**
		 * @copy com.yahoo.astra.fl.charts.IAxis#updateScale()
		 */
		override public function updateScale(data:Array):void
		{
			super.updateScale(data);
			
			if(this._categoryNamesSetByUser)
			{
				this._categoryLabels = [];
				var categoryCount:int = this._categoryNames.length;
				for(var i:int = 0; i < categoryCount; i++)
				{
					var category:String = this._categoryNames[i];
					this._categoryLabels[i] = this.valueToLabel(category);
				}
			}
			else
			{
				this.autoDetectCategories(data);
			}
		}
		
		/**
		 * @copy com.yahoo.astra.fl.charts.IAxis#updateBounds()
		 */
		override public function updateBounds():void
		{
			this.refreshLabels();
			
			super.updateBounds();
			
			this.calculateCategorySize();
		}
		
		/**
		 * @private
		 */
		override public function invalidate(property:String = InvalidationType.ALL, callLater:Boolean = true):void
		{
			//we need to make sure direct changes to this axis cause the whole chart to redraw!
			if(!inCallLaterPhase && this.parent is UIComponent) 
			{
				UIComponent(this.parent).invalidate();
			}
			super.invalidate(property, callLater);
		}
		
	//--------------------------------------
	//  Protected Methods
	//--------------------------------------
		
		/**
		 * @private
		 */
		override protected function draw():void
		{
			super.draw();
			
			this.updateBounds();
			
			this.graphics.clear();
			this.drawAxis();
			this.drawObjects();
		}
		
		/**
		 * @copy com.yahoo.astra.fl.charts.Axis#calculateContentBounds()
		 */
		override protected function calculateContentBounds():void
		{
			super.calculateContentBounds();
			
			var firstLabelWidth:Number = 0;
			var lastLabelWidth:Number = 0;
			var firstLabelHeight:Number = 0;
			var lastLabelHeight:Number = 0;
			if(this.labels.length > 0)
			{
				//get the first and last label
				var firstLabel:TextField = this.labels[0];
				firstLabelWidth = firstLabel.width;
				firstLabelHeight = firstLabel.height;
				var lastLabel:TextField = this.labels[this.labels.length - 1];
				lastLabelWidth = lastLabel.width;
				lastLabelHeight = lastLabel.height;
			}
			
			var showTicks:Boolean = this.getStyleValue("showTicks") as Boolean;
			var tickLength:Number = this.getStyleValue("tickLength") as Number;
			var tickPosition:String = this.getStyleValue("tickPosition") as String;
			var labelDistance:Number = this.getStyleValue("labelDistance") as Number;
			
			//build a fake category size based on blank metrics
			var tempCategorySize:Number = 0;
			var labelCount:int = this._categoryLabels.length; //we know labelCount > 0
			if(this.orientation == AxisOrientation.VERTICAL)
			{
				tempCategorySize = this.height / labelCount;
				
				var contentBoundsX:Number = this._maxLabelSize + labelDistance;
				
				var tickContentBoundsX:Number = 0;
				if(showTicks)
				{
					switch(tickPosition)
					{
						case TickPosition.OUTSIDE:
						case TickPosition.CROSS:
							tickContentBoundsX = tickLength;
							break;
					}
				}
				contentBoundsX += tickContentBoundsX;
				this._contentBounds.x += contentBoundsX;
				this._contentBounds.width -= contentBoundsX;
				
				var contentBoundsY:Number = 0;
				var firstLabelPosition:Number = (tempCategorySize / 2) - (firstLabelHeight / 2);
				if(firstLabelPosition < 0)
				{
					contentBoundsY = Math.abs(firstLabelPosition);
				}
				this._contentBounds.y += contentBoundsY;
				this._contentBounds.height -= contentBoundsY;
				
				var lastLabelMaxPosition:Number = (tempCategorySize * labelCount) - (tempCategorySize / 2) + (lastLabelHeight / 2);
				if(lastLabelMaxPosition > this.height)
				{
					this._contentBounds.height -= (lastLabelMaxPosition - this.height);
				}
			}
			else //horizontal
			{
				tempCategorySize = this.width / labelCount;
				
				contentBoundsX = 0;
				if(this.labels.length > 0)
				{
					firstLabelPosition = (tempCategorySize - firstLabelWidth) / 2;
					if(firstLabelPosition < 0)
					{
						contentBoundsX = Math.abs(firstLabelPosition);
					}
				}
				this._contentBounds.x += contentBoundsX;
				this._contentBounds.width -= contentBoundsX;
				
				if(this.labels.length > 0)
				{
					lastLabelMaxPosition = (tempCategorySize * labelCount) - (tempCategorySize / 2) + (lastLabelWidth / 2);
					if(lastLabelMaxPosition > this.width)
					{
						this._contentBounds.width -= (lastLabelMaxPosition - this.width);
					}
					this._contentBounds.height -= (this._maxLabelSize + labelDistance);
				}
				
				var tickHeight:Number = 0;
				if(showTicks)
				{
					switch(tickPosition)
					{
						case TickPosition.OUTSIDE:
						case TickPosition.CROSS:
							tickHeight = tickLength;
							break;
					}
				}
				this._contentBounds.height -= tickHeight;
			}
		}
	
		/**
		 * @private
		 * Determines the amount of space provided to each category.
		 */
		protected function calculateCategorySize():void
		{
			this._categorySize = 0;
			var labelCount:int = this._categoryLabels.length;
			if(this.orientation == AxisOrientation.VERTICAL)
			{
				this._categorySize = this._contentBounds.height;
				if(labelCount >= 0)
				{
					this._categorySize /= labelCount;
				}
			}
			else //horizontal
			{
				this._categorySize = this._contentBounds.width;
				if(labelCount >= 0)
				{
					this._categorySize /= labelCount;
				}
			} 
		}
	
		/**
		 * Draws the axis origin line.
		 */
		protected function drawAxis():void
		{
			var axisWeight:int = this.getStyleValue("axisWeight") as int;
			var axisColor:uint = this.getStyleValue("axisColor") as uint;
			this.graphics.lineStyle(axisWeight, axisColor);
			if(this.orientation == AxisOrientation.VERTICAL)
			{
				this.graphics.moveTo(this._contentBounds.x, this._contentBounds.y);
				this.graphics.lineTo(this._contentBounds.x, this._contentBounds.y + this._contentBounds.height);
			}
			else //horizontal
			{
				this.graphics.moveTo(this._contentBounds.x, this._contentBounds.y + this._contentBounds.height);
				this.graphics.lineTo(this._contentBounds.x + this._contentBounds.width, this._contentBounds.y + this._contentBounds.height);
			}
		}
		
		/**
		 * Draws the labels, ticks, and gridlines of the axis.
		 */
		protected function drawObjects():void
		{
			if(this.gridLines) this.gridLines.graphics.clear();
			
			//access the required styles
			var showLabels:Boolean = this.getStyleValue("showLabels") as Boolean;
			var labelDistance:Number = this.getStyleValue("labelDistance") as Number;
			var showGridLines:Boolean = this.getStyleValue("showGridLines") as Boolean;
			var gridLineWeight:int = this.getStyleValue("gridLineWeight") as int;
			var gridLineColor:uint = this.getStyleValue("gridLineColor") as uint;
			var showTicks:Boolean = this.getStyleValue("showTicks");
			var tickWeight:int = this.getStyleValue("tickWeight") as int;
			var tickColor:uint = this.getStyleValue("tickColor") as uint;
			var tickPosition:String = this.getStyleValue("tickPosition") as String;
			var tickLength:Number = this.getStyleValue("tickLength") as Number;
			
			var lastVisibleLabel:TextField;
			for(var i:int = 0; i < this.labels.length; i++)
			{
				var label:TextField = this.labels[i] as TextField;
				var category:Object = this.categoryNames[i];
				var position:Number = this.valueToLocal(category);
				
				//draw the grid lines
				if(this.gridLines && showGridLines)
				{
					this.gridLines.graphics.lineStyle(gridLineWeight, gridLineColor);
					//the gridlines are positioned at (contentBounds.x, contentBounds.y)
					if(this.orientation == AxisOrientation.VERTICAL)
					{
						this.gridLines.graphics.moveTo(0, position);
						this.gridLines.graphics.lineTo(this.contentBounds.width, position);
					}
					else
					{
						this.gridLines.graphics.moveTo(position, 0);
						this.gridLines.graphics.lineTo(position, this._contentBounds.height);
					}
				}
				
				if(this.orientation == AxisOrientation.VERTICAL)
				{
					position += this._contentBounds.y;
					label.x = this._contentBounds.x - label.width - labelDistance;
					label.y = position - label.height / 2;
				}
				else
				{
					position += this._contentBounds.x;
					label.x = position - label.width / 2;
					label.y = this._contentBounds.y + this._contentBounds.height + labelDistance;
				}
				
				if(showTicks)
				{
					this.graphics.lineStyle(tickWeight, tickColor);
					switch(tickPosition)
					{
						case TickPosition.OUTSIDE:
							if(this.orientation == AxisOrientation.VERTICAL)
							{
								this.graphics.moveTo(this._contentBounds.x - tickLength, position);
								this.graphics.lineTo(this._contentBounds.x, position);
								if(showLabels) label.x -= tickLength;
							}
							else
							{
								this.graphics.moveTo(position, this._contentBounds.y + this._contentBounds.height);
								this.graphics.lineTo(position, this._contentBounds.y + this._contentBounds.height + tickLength);
								if(showLabels) label.y += tickLength;
							}
							break;
						case TickPosition.INSIDE:
							if(this.orientation == AxisOrientation.VERTICAL)
							{
								this.graphics.moveTo(this._contentBounds.x, position);
								this.graphics.lineTo(this._contentBounds.x + tickLength, position);
							}
							else
							{
								this.graphics.moveTo(position, this._contentBounds.y + this._contentBounds.height - tickLength);
								this.graphics.lineTo(position, this._contentBounds.y + this._contentBounds.height);
							}
							break;
						default: //CROSS
							if(this.orientation == AxisOrientation.VERTICAL)
							{
								this.graphics.moveTo(this._contentBounds.x - tickLength, position);
								this.graphics.lineTo(this._contentBounds.x + tickLength, position);
								if(showLabels) label.x -= tickLength;
							}
							else
							{
								this.graphics.moveTo(position, this._contentBounds.y + this._contentBounds.height - tickLength);
								this.graphics.lineTo(position, this._contentBounds.y + this._contentBounds.height + tickLength);
								if(showLabels) label.y += tickLength;
							}
					}
				}
				
				if(showLabels && this.hideOverlappingLabels)
				{
					label.visible = true;
					if(this.orientation == AxisOrientation.VERTICAL)
					{
						if(lastVisibleLabel)
						{
							if(!this.reverse && label.y + label.height > lastVisibleLabel.y ||
								this.reverse && lastVisibleLabel.y + lastVisibleLabel.height > label.y)
							{
								label.visible = false;	
							}
						}
					}
					else
					{
						if(lastVisibleLabel)
						{
							if(this.reverse && label.x + label.width > lastVisibleLabel.x ||
								!this.reverse && lastVisibleLabel.x + lastVisibleLabel.width > label.x)
							{
								label.visible = false;	
							}
						}
					}
					if(label.visible) lastVisibleLabel = label;
				}
			} //end for loop
		}
		
	//--------------------------------------
	//  Private Methods
	//--------------------------------------
		
		/**
		 * @private
		 * Update the labels by adding or removing some, setting the text, etc.
		 */
		private function autoDetectCategories(data:Array):void
		{
			var uniqueCategoryValues:Array = [];
			var uniqueCategoryLabels:Array = [];
			var seriesCount:int = data.length;
			for(var i:int = 0; i < seriesCount; i++)
			{
				var series:ISeries = data[i] as ISeries;
				if(!series)
				{
					continue;
				}
				
				// determine the field for this axis
				var dataField:String = this.plotArea.axisAndSeriesToField(this, series);
				
				var seriesLength:int = series.length;
				for(var j:int = 0; j < seriesLength; j++)
				{
					var item:Object = series.dataProvider[j];
					if(!item)
					{
						continue;
					}
					
					var category:Object = j.toString(); //default: use the index
					if(item.hasOwnProperty(dataField))
					{
						//if we have a category field, use it
						//make sure the value is a string
						category = item[dataField].toString();
					}
					//labels must be unique
					var categoryLabel:String = this.valueToLabel(category);
					if(uniqueCategoryLabels.indexOf(categoryLabel) < 0)
					{
						uniqueCategoryLabels.push(categoryLabel);
						uniqueCategoryValues.push(category);
					}
				}
			}
			this._categoryLabels = uniqueCategoryLabels.concat();
			this._categoryNames = uniqueCategoryValues.concat();
		}
	
		/**
		 * @private
		 * Update the labels by adding or removing some, setting the text, etc.
		 */
		private function refreshLabels():void
		{
			var showLabels:Boolean = this.getStyleValue("showLabels") as Boolean;
			var labelCount:int = showLabels ? this._categoryLabels.length : 0;
			var difference:int = labelCount - this.labels.length;
			if(difference > 0)
			{
				//add new labels
				for(var i:int = 0; i < difference; i++)
				{
					var label:TextField = new TextField();
					label.selectable = false;
					label.autoSize = TextFieldAutoSize.LEFT;
					this.labels.push(label);
					this.addChild(label);
				}
			}
			else if(difference < 0)
			{
				//remove existing labels
				difference = Math.abs(difference);
				for(i = 0; i < difference; i++)
				{
					label = this.labels.pop() as TextField;
					this.removeChild(label);
				}
			}
			
			var textFormat:TextFormat = this.getStyleValue("textFormat") as TextFormat;
			for(i = 0; i < labelCount; i++)
			{
				label = this.labels[i] as TextField;
				label.defaultTextFormat = textFormat;
			}
			
			//with the labels in place, update the displayed text
			if(showLabels)
			{
				this.setLabelText();
			}
		}
		
		/**
		 * @private
		 * Sets the text for each label and determines the offets.
		 */
		private function setLabelText():void
		{
			this._maxLabelSize = 0;
			for(var i:Number = 0; i < this.labels.length; i++)
			{
				var label:TextField = this.labels[i] as TextField;
				
				var category:String = i.toString();
				if(this._categoryLabels && this._categoryLabels.length > 0)
				{
					category = this._categoryLabels[i];
					if(!category) category = "";
				}
				label.text = category;
				
				if(this.orientation == AxisOrientation.VERTICAL)
				{
					this._maxLabelSize = Math.max(this._maxLabelSize, label.width);
				}
				else this._maxLabelSize = Math.max(this._maxLabelSize, label.height);
			}
		}
		
	}
}
